Skip to content

feat(billing): instant referee grant on organization provisioning#3859

Merged
PierreBrisorgueil merged 6 commits into
masterfrom
fix/3844-instant-referee-grant
Jun 13, 2026
Merged

feat(billing): instant referee grant on organization provisioning#3859
PierreBrisorgueil merged 6 commits into
masterfrom
fix/3844-instant-referee-grant

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Instant referee referral grant — with a mailer configured the referee's org is provisioned at email verification (after invitation.accepted), so the referee grant previously landed no_organization and waited ≤24h for the reconcile cron. This makes it instant; the cron stays the safety net.

  • New modules/organizations/lib/events.jsorganizationEvents singleton + organization.provisioned event { userId, organizationId }; emitted at the end of handleSignupOrganization whenever a non-null org results (sync try/catch). Error listener registered in organizations.init.js.
  • billing.init.js — a second listener (config-gated on billing.referral.enabled FIRST, lazy dynamic imports, fully self-guarded — never rejects): on organization.provisioned, if the user has referredBy, resolve their accepted invitation (InvitationRepository.findByAcceptedUserId) and re-run grantForInvitation (idempotent via the referral:<invitationId>:* ledger refId — double-fire with the listener/cron is duplicate_grant).
  • Dependency direction stays one-way: billing imports the organizations events singleton; the new organizations events/init do not import billing.

Test plan

organizations.service.signup (29), invitations.repository.unit (14), billing.init.unit (22), billing.referral.integration (4, incl. a mailer-on instant-grant timing test). Full unit 1989 + integration 1837, 0 failed. Lint clean.

Guardrails

  • npm run lint clean
  • Public-OSS clean (also neutralized a pre-existing downstream name in the touched billing.init.js comments)
  • Async listener never rejects; grant idempotent

Closes #3844

Summary by CodeRabbit

  • New Features
    • Referral credits are now granted when organizations are provisioned, ensuring referrers receive compensation even if referral billing was disabled at invitation acceptance time. This closes a timing gap in the referral system.

New config-free singleton lib/events.js (mirrors invitations/lib/events.js)
emitted at every handleSignupOrganization exit path that returns a real
organization — fresh create AND A4 convergence (consumers must be idempotent;
double-fire is by design). Sync try/catch at the emit site; the mandatory
'error' listener lives in the new organizations.init.js (auto-discovered via
the modules/*/*.init.js glob). With a mailer configured this fires at email
verification, the exact moment a referral grant becomes possible.

refs #3844
Lean one-row lookup { status:'accepted', acceptedUserId } with the same minimal
projection as findAccepted — resolves the referral idempotency keys from a user
id at organization-provisioning time (user.referredBy alone cannot: it carries
the inviter but not the invitationId the ledger keys need).

refs #3844
With a mailer configured the referee's organization is provisioned at email
verification — after invitation.accepted fired — so the #3842 listener landed
no_organization and the referee grant waited for the reconcile cron (up to 24h).
New config-gated, self-guarded listener re-runs the idempotent grantForInvitation
at the exact provisioning moment (ledger refId guard makes listener/cron
double-fire harmless); the cron stays the truth/safety net.

refs #3844
Reproduces the email-verification gap (accepted invite + referredBy, no ledger
keys), re-runs handleSignupOrganization as verifyEmail does (A4 convergence), and
asserts the organization.provisioned listener credits the referee instantly with
replay coming back duplicate_grant.

refs #3844
Replaces a named downstream consumer in two billing-event comments with

neutral wording (public-OSS no-downstream-refs rule). refs #3844
Copilot AI review requested due to automatic review settings June 13, 2026 16:24
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

This PR implements instant crediting of referral rewards when organizations are provisioned at email verification. It adds an organization.provisioned event emitted from the organizations service and wired a config-gated billing listener that re-invokes referral grant logic idempotently, covering the mailer-on signup timing gap between invitation acceptance and org provisioning.

Changes

Instant Referee Grant with Organization Provisioning Event

Layer / File(s) Summary
Event infrastructure for organization provisioning
modules/organizations/lib/events.js, modules/organizations/organizations.init.js
New organizationEvents singleton is created and initialized with an error handler to log uncaught listener failures during bootstrap.
Invitation lookup by accepted user ID
modules/invitations/repositories/invitations.repository.js, modules/invitations/tests/invitations.repository.unit.tests.js
InvitationRepository.findByAcceptedUserId() helper validates user ID, queries for accepted invitations with minimal projection, and includes unit tests for valid/invalid ID paths.
Organization provisioning event emission
modules/organizations/services/organizations.service.js, modules/organizations/tests/organizations.service.signup.unit.tests.js
emitProvisioned helper emits organization.provisioned with userId and organizationId in all signup completion paths (disabled orgs, enabled orgs, convergence), catching listener throws; unit tests verify emission timing and resilience.
Billing instant referee grant listener
modules/billing/billing.init.js, modules/billing/tests/billing.init.unit.tests.js
Config-gated listener on organization.provisioned lazily imports services, verifies referredBy, looks up accepted invitation, and calls BillingReferralService.grantForInvitation idempotently; wrapped tests cover config gating, missing data, happy path, and listener resilience.
Integration testing of provisioning + referral timing
modules/billing/tests/billing.referral.integration.tests.js
New test validates mailer-on timing by disabling referral at accept, enabling after verification, and confirming instant referee credit at provisioning; verifies idempotency and single ledger entry per key; also optimizes concurrent grant test settling.

Sequence Diagram

sequenceDiagram
  participant User as User (Email Verify)
  participant AuthService
  participant OrganizationsService
  participant organizationEvents
  participant BillingListener
  participant UserService as UserService (Referral)
  participant InvitationRepository
  participant BillingReferralService
  User->>AuthService: verifyEmail()
  AuthService->>OrganizationsService: handleSignupOrganization(user)
  OrganizationsService->>OrganizationsService: create/sync org
  OrganizationsService->>organizationEvents: emit('organization.provisioned',<br/>{userId, organizationId})
  organizationEvents->>BillingListener: listener invoked
  BillingListener->>UserService: getBrut(userId)
  UserService-->>BillingListener: user {referredBy}
  BillingListener->>InvitationRepository: findByAcceptedUserId(userId)
  InvitationRepository-->>BillingListener: {invitedBy, acceptedUserId}
  BillingListener->>BillingReferralService: grantForInvitation(invitedBy,<br/>acceptedUserId, organizationId)
  BillingReferralService-->>BillingListener: grant result
  Note over BillingListener: Catch errors, log, never reject
  BillingListener-->>organizationEvents: listener complete
  Note over User: Referee sees +500 credits instantly
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • pierreb-devkit/Node#3765: Adds the call to AuthOrganizationService.handleSignupOrganization(user) during verifyEmail, which triggers the organization provisioning path that emits organization.provisioned that this PR's billing listener consumes.
  • pierreb-devkit/Node#3824: Adds the invitation.accepted referral grant listener in billing.init.js that this PR supplements with an organization.provisioned listener to handle the mailer-on signup timing gap.
  • pierreb-devkit/Node#3680: Modifies the same organizations.service.js provisioning paths around signup flows that this PR extends to emit the organization.provisioned event.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(billing): instant referee grant on organization provisioning' clearly and concisely describes the main change—adding instant referral credit grants upon organization provisioning rather than waiting for cron.
Description check ✅ Passed The PR description covers all key template sections: summary (what/why), scope (modules impacted, risk level implied), validation (tests and lint results reported), guardrails (lint, OSS, idempotency, async safety), and closes linked issue #3844.
Linked Issues check ✅ Passed All coding objectives from #3844 are fully implemented: organizations.provisioned event emitter created [#3844], billing listener added with config-gating and idempotency [#3844], dependency directions preserved [#3844], and comprehensive tests added [#3844].
Out of Scope Changes check ✅ Passed All changes directly support the #3844 objective of instant referee grants on org provisioning: event emitter, billing listener, invitation lookup helper, and comprehensive tests. No unrelated refactoring or scope creep detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3844-instant-referee-grant

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.41%. Comparing base (12b961c) to head (b94f736).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3859      +/-   ##
==========================================
+ Coverage   92.37%   92.41%   +0.04%     
==========================================
  Files         160      162       +2     
  Lines        5361     5394      +33     
  Branches     1723     1736      +13     
==========================================
+ Hits         4952     4985      +33     
  Misses        328      328              
  Partials       81       81              
Flag Coverage Δ
integration 60.10% <87.87%> (+0.17%) ⬆️
unit 73.17% <93.93%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 12b961c...b94f736. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an organizations-scoped provisioning event so billing can immediately re-attempt the referee referral grant at the exact moment an organization becomes available (notably in the mailer-on “org provisioned at email verification” flow), while keeping the existing reconcile cron as a safety net and preserving the one-way dependency direction (billing → organizations).

Changes:

  • Introduces organization.provisioned as a config-free, import-safe singleton event (modules/organizations/lib/events.js) and registers an error listener in organizations.init.js.
  • Emits organization.provisioned from OrganizationsService.handleSignupOrganization whenever a non-null organization is returned (fresh create + A4 convergence path), swallowing synchronous listener throws.
  • Adds a billing-side listener (config-gated on config.billing.referral.enabled) that resolves the accepted invitation for the provisioned user and re-runs the idempotent referral grant; includes unit + integration coverage for the mailer-on timing gap.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
modules/organizations/services/organizations.service.js Emits organization.provisioned on successful org provisioning/convergence with sync-throw protection.
modules/organizations/lib/events.js Adds config-free organizationEvents singleton and documents async-listener self-guard requirements.
modules/organizations/organizations.init.js Registers mandatory error listener for the organizations event emitter during module init.
modules/organizations/tests/organizations.service.signup.unit.tests.js Adds unit coverage asserting emit behavior (once, correct ids, no emit on mailer gating, sync throw swallowed).
modules/invitations/repositories/invitations.repository.js Adds findByAcceptedUserId helper for resolving the accepted invitation by user id (lean + minimal projection).
modules/invitations/tests/invitations.repository.unit.tests.js Adds unit coverage for findByAcceptedUserId query/projection/lean behavior.
modules/billing/billing.init.js Wires new organization.provisioned listener to re-attempt referee grant (config-gated, lazy imports, never rejects).
modules/billing/tests/billing.init.unit.tests.js Adds unit coverage for the new billing listener wiring, gating, and self-guard behavior.
modules/billing/tests/billing.referral.integration.tests.js Adds integration test proving instant grant on org provisioning after acceptance (mailer-on timing), plus replay idempotency.

@PierreBrisorgueil

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

H1: add mongoose.Types.ObjectId.isValid guard to findByAcceptedUserId
(mirrors the sibling get() pattern); invalid id returns null without
hitting the DB. Add unit test asserting invalid id → null + no findOne.

M2: replace two setTimeout(resolve, 300) disabled-listener sleeps in
billing.referral.integration with setImmediate drain — the 300ms was
guarding a DISABLED listener that returns immediately, so a microtask
drain is sufficient and deterministic.

refs #3844
@PierreBrisorgueil PierreBrisorgueil merged commit 7d4f605 into master Jun 13, 2026
7 of 8 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the fix/3844-instant-referee-grant branch June 13, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ instant referee grant: retry grantForInvitation when the org is provisioned at email verification

2 participants